/*
 * Niiuzu
 * Copyright (C) 2011  Thierry Nowak
 * 
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.niiuzu.server.util;

import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.niiuzu.shared.model.FeedChannel;
import com.niiuzu.shared.model.FeedChannelType;
import com.niiuzu.shared.model.FeedEntry;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.XmlReader;

/**
 * Moteur de récupération des données depuis des flux RSS renseignés par
 * l'utilisateur.
 */
public class FeedDataRetriever {

	private static final Logger LOGGER = Logger
			.getLogger(FeedDataRetriever.class.getName());

	static final String TYPE_ATOM_V0_3 = "atom_0.3";
	static final String TYPE_ATOM_V1 = "atom_1.0";
	static final String TYPE_RSS_V0_90 = "rss_0.9";
	static final String TYPE_RSS_V0_91 = "rss_0.91";
	static final String TYPE_RSS_V0_91_NETSCAPE = "rss_0.91N";
	static final String TYPE_RSS_V0_91_USERLAND = "rss_0.91U";
	static final String TYPE_RSS_V0_92 = "rss_0.92";
	static final String TYPE_RSS_V0_93 = "rss_0.93";
	static final String TYPE_RSS_V0_94 = "rss_0.94";
	static final String TYPE_RSS_V1 = "rss_1.0";
	static final String TYPE_RSS_V2 = "rss_2.0";

	/**
	 * Convertion du type de flux RSS d'un format {@link String} vers une
	 * énumération.
	 * 
	 * @return l'énumération correspondant au type, ou bien <b>null</b> si
	 *         aucune correspondance n'a été trouvée.
	 */
	public static FeedChannelType convertTypeStringToEnum(String feedType) {

		if (feedType == null) {
			return null;
		} else if (feedType.equals(TYPE_ATOM_V0_3)) {
			return FeedChannelType.ATOM_V0_03;
		} else if (feedType.equals(TYPE_ATOM_V1)) {
			return FeedChannelType.ATOM_V1_00;
		} else if (feedType.equals(TYPE_RSS_V0_90)) {
			return FeedChannelType.RSS_V0_90;
		} else if (feedType.equals(TYPE_RSS_V0_91)) {
			return FeedChannelType.RSS_V0_91;
		} else if (feedType.equals(TYPE_RSS_V0_91_NETSCAPE)) {
			return FeedChannelType.RSS_V0_91_NETSCAPE;
		} else if (feedType.equals(TYPE_RSS_V0_91_USERLAND)) {
			return FeedChannelType.RSS_V0_91_USERLAND;
		} else if (feedType.equals(TYPE_RSS_V0_92)) {
			return FeedChannelType.RSS_V0_92;
		} else if (feedType.equals(TYPE_RSS_V0_93)) {
			return FeedChannelType.RSS_V0_93;
		} else if (feedType.equals(TYPE_RSS_V0_94)) {
			return FeedChannelType.RSS_V0_94;
		} else if (feedType.equals(TYPE_RSS_V1)) {
			return FeedChannelType.RSS_V1_00;
		} else if (feedType.equals(TYPE_RSS_V2)) {
			return FeedChannelType.RSS_V2_00;
		}

		return null;
	}

	/**
	 * Créer un objet {@link FeedChannel} à partir d'une URL pointant vers un
	 * flux RSS qui aura été vérifié au préalable.
	 * 
	 * @throws IOException
	 *             Problème technique rencontré à la création du flux XML.
	 * @throws FeedException
	 *             Lecture du flux de données impossible.
	 * @throws IllegalArgumentException
	 *             Impossible de parser le flux de données récupéré.
	 */
	public FeedChannel createChannelFromLink(String channelLink)
			throws IllegalArgumentException, IOException, FeedException {

		SyndFeedInput syndFeedInput = createSyndFeedInput();
		SyndFeed syndFeed = createSyndFeed(channelLink, syndFeedInput);

		IdBuilder idBuilder = IdBuilder.getInstance();

		// Mapping
		FeedChannel channel = new FeedChannel();
		channel.setId(idBuilder.getNewId(FeedChannel.class));
		channel.setLink(channelLink);
		channel.setName(syndFeed.getTitle());
		FeedChannelType type = convertTypeStringToEnum(syndFeed.getFeedType());
		channel.setType(type);

		return channel;
	}

	/**
	 * Récupère les dernières actualités depuis une liste de flux RSS.
	 * 
	 * Cette méthode se base sur tous les flux qui lui sont renseignées pour
	 * récupérer les dernières actualités, les derniers articles. En revanche
	 * elle ne propage pas les erreurs rencontrées lors de l'interrogation du
	 * flux.
	 */
	public List<FeedEntry> getEntriesFromChannels(FeedChannel... channels) {

		List<FeedEntry> entries = new LinkedList<FeedEntry>();

		SyndFeedInput syndFeedInput = createSyndFeedInput();

		IdBuilder idBuilder = IdBuilder.getInstance();

		// Parcours des flux (en cas d'erreur, on ne brise pas la boucle)
		for (FeedChannel channel : channels) {
			String channelLink = channel.getLink();

			try {

				SyndFeed syndFeed = createSyndFeed(channelLink, syndFeedInput);

				// Parcours des entrées reçues
				for (Object oEntry : syndFeed.getEntries()) {
					SyndEntry syndEntry = (SyndEntry) oEntry;

					// Mapping
					FeedEntry newEntry = new FeedEntry();
					newEntry.setId(idBuilder.getNewId(FeedEntry.class));
					newEntry.setChannelId(channel.getId());
					newEntry.setDate(syndEntry.getPublishedDate());
					newEntry.setLink(syndEntry.getLink());
					newEntry.setSummary(syndEntry.getTitle());
					// TODO calculer la description complète
					newEntry.setCompleteDescription("");

					entries.add(newEntry);
				}

			} catch (IOException e) {
				String message = String.format(
						"Cannot read the stream of the channel URL [%s]",
						channelLink);
				LOGGER.log(Level.SEVERE, message, e);
			} catch (IllegalArgumentException e) {
				String message = String.format(
						"Feed type could not be understood by parsers [%s]",
						channelLink);
				LOGGER.log(Level.SEVERE, message, e);
			} catch (FeedException e) {
				String message = String.format("Feed could not be parsed [%s]",
						channelLink);
				LOGGER.log(Level.SEVERE, message, e);
			}

		}

		// Tri du résultat
		Collections.sort(entries, new Comparator<FeedEntry>() {
			@Override
			public int compare(FeedEntry o1, FeedEntry o2) {

				// Tri décroissant sur les dates
				Date d1 = o1.getDate();
				Date d2 = o2.getDate();

				if (d1 != null && d2 != null) {
					return (-1) * d1.compareTo(d2);
				} else if (d1 == null && d2 == null) {
					return 0;
				} else if (d1 == null) {
					return Integer.MAX_VALUE;
				} else {
					return Integer.MIN_VALUE;
				}
			}
		});

		return entries;
	}

	/**
	 * Récupère un {@link SyndFeed} contenant toutes les informations
	 * nécessaires à l'interrogation d'un flux RSS dont le lien est passé en
	 * paramètre.
	 * 
	 * @param channelLink
	 *            Lien vers le flux RSS.
	 * @param input
	 *            Instance de {@link SyndFeedInput} nécessaire à l'interrogation
	 *            du flux.
	 * @throws IOException
	 *             Problème technique rencontré à la création du flux XML.
	 * @throws FeedException
	 *             Lecture du flux de données impossible.
	 * @throws IllegalArgumentException
	 *             Impossible de parser le flux de données récupéré.
	 */
	private SyndFeed createSyndFeed(String channelLink, SyndFeedInput input)
			throws IOException, IllegalArgumentException, FeedException {

		URL channelUrl = new URL(channelLink);

		// Interrogation du flux
		XmlReader rssXmlReader = createXmlReader(channelUrl);
		SyndFeed syndFeed = input.build(rssXmlReader);

		return syndFeed;
	}

	/**
	 * Demande la création d'une instance d'un {@link SyndFeedInput}.
	 */
	private SyndFeedInput createSyndFeedInput() {
		if (mockedSyndFeedInput != null) {
			return mockedSyndFeedInput;
		}
		return new SyndFeedInput();
	}

	/**
	 * Demande la création d'une instance d'un {@link XmlReader}.
	 * 
	 * @throws IOException
	 *             Problème technique rencontré à la lecture du flux XML.
	 */
	private XmlReader createXmlReader(URL channelUrl) throws IOException {
		if (mockedXmlReader != null) {
			return mockedXmlReader;
		}
		return new XmlReader(channelUrl);
	}

	/* ********************************************************************** */
	/* ********************************************************************** */
	/* ********************************************************************** */

	private SyndFeedInput mockedSyndFeedInput;
	private XmlReader mockedXmlReader;

	/**
	 * JUnit - Définit un mock pour {@link SyndFeedInput}.
	 */
	public void setMockSyndFeedInput(SyndFeedInput syndFeedInput) {
		this.mockedSyndFeedInput = syndFeedInput;
	}

	/**
	 * JUnit - Définit un mock pour {@link XmlReader}.
	 */
	public void setMockXmlReader(XmlReader xmlReader) {
		this.mockedXmlReader = xmlReader;
	}

}
